.. _Tutorial: using NeurEco Python API on a Tabular Compression problem:


Tutorial: using NeurEco Python API on a Tabular Compression problem
===================================================================

The following section uses the test case :std:ref:`Heaviside test case`. This test case is included in the NeurEco installation package.

* Create an empty directory (Heaviside Example), extract the :std:ref:`Heaviside test case` test case data there. The created directory contains the following files:

* x_test.csv
* x_train.csv

* Import the required libraries (NeurEco and NumPy):

.. code-block:: python

  from NeurEco import NeurEcoTabular as Tabular
  import numpy as np

* Load the training data:

.. code-block:: python

  x_train = np.genfromtxt("x_train.csv", delimiter=";", skip_header=True)

* Initialize a NeurEco object to handle the **Compression** problem:

.. code-block:: python

  builder = Tabular.Compressor()

All the methods provided by the Compressor class, can be viewed by calling the *__method__* attributes:

.. code-block:: python

  print(builder.__methods__)

.. code-block:: text
    
  **** NeurEco Tabular Compressor methods: ****
  - load
  - save
  - delete
  - evaluate
  - build
  - get_input_count
  - get_output_count
  - load_model_from_checkpoint
  - get_number_of_networks_from_checkpoint
  - get_weights
  - export_fmu
  - export_c
  - export_onnx
  - export_vba
  - compute_error
  - separate_models
  - concatenate_models
  - plot_network
  - plot_compression_coefficients
  - forward_derivative
  - gradient
  - set_weightsplot_compression_coefficients
  - perform_input_sweep

To understand what each parameter of any method does and how to use it, print the doc of the method:

.. code-block:: python

  print(builder.export_c.__doc__)

.. code-block:: text

    exports a NeurEco tabular model to a header file
    :param h_file_path: path where the .h file will be saved
    :param precision: string: optional: "float" or "double": precision of the weights in the h file
    :return: export_status: int: 0 if export is ok, other if otherwise.

* To build the model, run the **build** method with the building parameters adjusted to the problem at hand (see :std:ref:`Build NeurEco Compression model with the Python API`). For this example, the outputs are normalized per feature (meaning that each output is normalized apart, it is the default setting for **Compression**, see :std:ref:`Normalizing the data Tabular Compression`):

.. code-block:: python

  builder.build(x_train,  # the rest of the parameters are optional
            write_model_to='./HeavisideModel/Heaviside.ednn',
            write_compression_model_to='./HeavisideModel/HeavisideCompressor.ednn',
            write_decompression_model_to='./HeavisideModel/HeavisideUncompressor.ednn',
            compress_tolerance=0.050,
            checkpoint_address='./HeavisideModel/Heaviside.checkpoint',
            final_learning=True,
            initial_beta_reg=0.1)

* When **build** is called, NeurEco starts the building process:

.. code-block:: text

  Validation Percentage will be used to get the validation data. This is due to:
  - one or all the validation data is set to None
  - validation indices is set to None

  info >
  info >      _   __                ______
  info >     / | / /__  __  _______/ ____/________
  info >    /  |/ / _ \/ / / / ___/ __/ / ___/ __ \
  info >   / /|  /  __/ /_/ / /  / /___/ /__/ /_/ /
  info >  /_/ |_/\___/\__,_/_/  /_____/\___/\____/
  info >                  === A D A G O S ===
  info >
  info > Version: 4.01.2474.0 Compiled with MSVC v1928  Oct 12 2022 Matlab runtime:no
  info > OpenMP: yes
  info > MKL: yes
  info > Reading data files...
  info > Reading Data from C:/Users/Sadok/AppData/Local/Temp/tmpy1bh9n82/inputs_tab_comp_train.npy
  info > build for: 20 outputs and 20 inputs and 400 samples.

During the build NeurEco saves the intermediate modes to the checkpoint file (defined by the parameter **checkpoint_address**). 
To load and use the intermediate models from this checkpoint: 

* Create a new NeurEco object in which to load the model:

.. code-block:: python

  model = Tabular.Compressor()

* Determine how many intermediate models the checkpoint contains:

.. code-block:: python

  n = model.get_number_of_networks_from_checkpoint("./HeavisideModel/Heaviside.checkpoint")

* Load any intermediate model from the checkpoint using its id (count starts with zero). For this example, at the moment of running the command :math:`n=2` and the following command loads the intermediate model :math:`n°1 \ (id=0)`:

.. code-block:: python

  model.load_model_from_checkpoint("./HeavisideModel/Heaviside.checkpoint", 0)

Now **model** is a valid **Compression** model, and can be used as usual. 

* Check the number of trainable parameters each of the intermediate models has:

.. code-block:: python

  for i in range(n):
      print("Loading model", i, " from checkpoint file:")
      model.load_model_from_checkpoint("./HeavisideModel/Heaviside.checkpoint", i)
      print("number of trainable parameters in intermediate model --", i, " is:", model.get_weights().size)

.. code-block:: text

  Loading model 0  from checkpoint file:
  number of trainable parameters in intermediate model -- 0  is: 676
  Loading model 1  from checkpoint file:
  number of trainable parameters in intermediate model -- 1  is: 1220
  Loading model 2  from checkpoint file:
  number of trainable parameters in intermediate model -- 2  is: 1731
  Loading model 3  from checkpoint file:
  number of trainable parameters in intermediate model -- 3  is: 2145
  Loading model 4  from checkpoint file:
  number of trainable parameters in intermediate model -- 4  is: 2157
  Loading model 5  from checkpoint file:
  number of trainable parameters in intermediate model -- 5  is: 2157

Once the build is over, we can move to evaluating it on new data. To do so, we will start by separating it into a compression model and a decompression model, which are both regression models in this case. This is done by either loading them from the disk separately (in the build we asked for each model to be saved separately), or we can call the method separate_models of a NeurEco Compressor. We will load the model first:

* Create a **Compressor** object to use for the evaluation:

.. code-block:: python

  combined_model = Tabular.Compressor()

.. note::
    It is possible to use the already existing **Compressor** object **builder** when the evaluation is done just after the **build**, and **builder** is still available.

* Load the built model:
  
.. code-block:: python

  combined_model.load("./HeavisideModel/Heaviside.ednn")
  
* To separate the **Compressor** model into two parts: a **Regressor** model for compression part and a **Regressor** model for the decompression part:

  * Create two new **Regressor** objects to use:

  .. code-block:: python

    neurEco_Compressor = Tabular.Regressor()
    neurEco_Decompressor = Tabular.Regressor()

  * Separate the **Compressor** model into a compressor and a decompressor:

  .. code-block:: python

    separate_status = combined_model.separate_models(neurEco_Compressor, neurEco_Decompressor)

.. note::
    When building or evaluating a NeurEco model, all the used paths do not necessarily need to have an extension when they are passed as parameters to a NeurEco method.

* Evaluate the separated models on the testing data and compute the compression error:

.. code-block:: python

  compressed_coefficients = neurEco_Compressor.evaluate(x_test)
  decompressed_output = neurEco_Decompressor.evaluate(compressed_coefficients)
  compression_error = neurEco_Decompressor.compute_error(decompressed_output, x_test)
  print("The non-linear compression error is (%):", 100 * compression_error)
 
.. code-block:: text
    
  The non-linear compression error is (%): 1.898570291522237

.. note::
  - The relative compression error is highly dependent on the tolerance chosen for the **build**. A smaller tolerance leads to a more complex model and a better error on the testing set and the training set.
  - During evaluation, the normalization is carried out by the model and its parameters are not relative to the data set being evaluated, but are the global parameters computed during the **build** of the model.
  - The obtained result **decompressed_output** is the same as the output of the call:
  
  .. code-block:: python
  
    decompressed_output_combined = combined_model.evaluate(x_test)
  

* The **neurEco_Compressor** and **neurEco_Decompressor** models can be used as regular **Regression** models. For example, to extract the information about **neurEco_Compressor**, run:

.. code-block:: python

  n_inputs = neurEco_Compressor.get_input_count()
  n_outputs = neurEco_Compressor.get_output_count()
  weights = neurEco_Compressor.get_weights()
  print("Number of inputs for the compression model:", n_inputs)
  print("Number of nonlinear coefficients:", n_outputs)
  print("Number of trainable parameters - compression block:", weights.size)


.. code-block:: text

  Number of inputs for the compression model: 20
  Number of nonlinear coefficients: 2
  Number of trainable parameters - compression block: 901

* Plot the network graph (see :std:ref:`Plot a NeurEco network Compression with the Python API`, this operation requires *matplotlib* library installed) for any model (compressor, decompressor or combined):

.. code-block:: python

  combined_model.plot_network()

.. figure:: ./images/HeavisidePythonNetworkPlot.png
  :width: 800
  :alt: HeavisidePythonNetworkPlot
  :align: center

  Python API operations: plotting a network: test case - Heaviside


* Plot the compression coefficients (this operation requires *matplotlib* library installed). 

For this test case (:std:ref:`Heaviside test case`), one of the parameters (:math:`a1`) represents the amplitude of the signal. All the amplitudes are contained in the interval [-0.6, 1]. Let’s get all the amplitude higher than 0.2 and all the amplitude lower than 0.2 separated (0.2 is the middle point of the interval). 

.. code-block:: python

  amplitudes = []
  for i in range(x_train.shape[0]):
      sample = x_train[i, :]
      diff = np.diff(sample)
      amplitudes.append(np.max(sample))
  amplitudes = np.array(amplitudes)
  high_amplitudes_indexes = np.where(amplitudes > 0.2)[0]
  low_amplitude_indexes = np.where(amplitudes < 0.2)[0]


Create a binary set of classes “h” and “l” for higher and lower, and plot the compression coefficients for these classes.

.. code-block:: python

  classes = np.empty(x_train.shape[0]).astype(str)
  classes[high_amplitudes_indexes] = "h"
  classes[low_amplitude_indexes] = "l"
  combined_model.plot_compression_coefficients(data_to_compress=x_train, neurons_ids=[0, 1], data_labels=list(classes))


.. figure:: ./images/HeavisidePythonPlotCompressionCoeffs.png
  :width: 800
  :alt: HeavisidePythonPlotCompressionCoeffs
  :align: center

  Python API operations: Plotting the nonlinear coefficients: test case - Heaviside

* To perform an input sweep (see :std:ref:`Input sweep Compression python`, this operation requires *matplotlib* library installed), run, for example:

.. code-block:: python

  combined_model.perform_input_sweep(x=x_test[43, :], input_id=2, input_interval=[-1, 1], output_id=0)

.. figure:: ./images/HeavisidePythonInputSweep.png
  :width: 800
  :alt: HeavisidePythonInputSweep
  :align: center

  Python API operations: Performing an input sweep: test case - Heaviside

* To save the model in the native NeurEco binary format:

.. code-block:: python

  save_state = combined_model.save("Heaviside/NewDir/SameModel")

* To export the model, run one of the following commands (*embed* license is required):

.. code-block:: python

  combined_model.export_c("./HeavisideModel/Heaviside.h", precision="float")
  combined_model.export_onnx("./HeavisideModel/Heaviside.onnx", precision="float")
  combined_model.export_fmu("./HeavisideModel/Heaviside.fmu")
  combined_model.export_vba("./HeavisideModel/Heaviside.bas")
  
.. note::
  The compressor and decompressor parts of the **combined_model** are exported automatically as well.

.. warning::
  Once the NeurEco object is no longer needed, free the memory by deleting the object by calling the **delete** method. For the example above, five objects must be deleted

  .. code-block:: python

    builder.delete()
    combined_model.delete()
    model.delete()
    neurEco_Compressor.delete()
    neurEco_Decompressor.delete()
